/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

import { SpawnSyncOptions, SpawnSyncReturns } from 'child_process';
import path from 'path';

import { cloneDeep, noop } from '@baize/wdk';
import { FileSet, hvigorCore, MetricLogType, replaceBundleName, TaskDetails, TaskInputValue } from '@ohos/hvigor';
import fse from 'fs-extra';
import { levels } from 'log4js';


import { parsingProfileName } from '../../common/common-util.js';
import { projectOhosConfigManager } from '../../common/global/project-ohos-config-manager.js';
import { ohosTrace } from '../../common/trace/ohos-trace.js';
import { BuildArtifactConst, BuildDirConst } from '../../const/build-directory-const.js';
import { CommonConst } from '../../const/common-const.js';
import { InjectConst } from '../../const/inject-const.js';
import { ProjectModelImpl } from '../../model/project/project-model-impl.js';
import { ConfigJson } from '../../options/configure/config-json-options.js';
import { ModuleJson } from '../../options/configure/module-json-options.js';
import { RestoolLinkParamObj } from '../../options/configure/restool-config-options.js';
import { CompileResourceBuildCommandType, CompileResourcesUtil } from '../../utils/compile-resources-util.js';
import { FileUtil } from '../../utils/file-util.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';
import { strMapToObj } from '../../utils/map-util.js';
import { ProcessUtils } from '../../utils/process-utils.js';
import { ModuleTargetData } from '../data/hap-task-target-data.js';
import { checkHasTargetApplyProduct } from '../service/module-task-service.js';
import { TargetTaskService } from '../service/target-task-service.js';

import ConfigOptObj = ConfigJson.ConfigOptObj;
import ModuleOptObj = ModuleJson.ModuleOptObj;
import WantsObj = ModuleJson.WantsObj;
import ShortcutsObj = ModuleJson.ShortcutsObj;

import { getJson5Obj } from '../../utils/json-util.js';
import { resModelLoader } from '../../utils/loader/file/res-model-loader.js';
import { AbstractResource } from './abstract-resource-task.js';

/**
 * 资源编译任务的公共抽象类
 *
 * @since 2022/9/8
 */
export abstract class AbstractCompileResource extends AbstractResource {
  protected intermediatesResDir: string;
  protected generateSourceRDir: string | undefined;

  protected resConfigFilePath: string | undefined;

  protected entryModuleName: string | undefined;

  protected compileCommands: string[][] = [];
  protected linkCommand: string[] | undefined = [];
  protected linkOutputPath: string | undefined;
  protected isPreview: boolean | undefined;

  restoolLinkParamObj: RestoolLinkParamObj | undefined;

  protected abstract processProfileTask: string;

  protected constructor(taskService: TargetTaskService, taskDetails: TaskDetails) {
    super(taskService, taskDetails);
    this.intermediatesResDir = this.pathInfo.getIntermediatesRes();
    this.isPreview = hvigorCore.getExtraConfig().get(InjectConst.BUILD_ROOT) === BuildDirConst.PREVIEW_BUILD_PATH;
  }

  getEnabled(): boolean {
    const processProfile = this.node.getTaskContainer().findTask(`${this.targetName}@${this.processProfileTask}`)! as AbstractResource;
    if (processProfile === undefined) {
      throw new Error(`Resource compile task initialize failed, task ${this.processProfileTask} not found.`);
    }
    this.restoolCommandBuilder = processProfile.restoolCommandBuilder;
    this.restoolConfigBuilder = processProfile.restoolConfigBuilder;
    return super.getEnabled();
  }

  protected getCommand(): string[] {
    return [this.getResToolFile(), '-l', this.resConfigFilePath!];
  }

  protected beforeTask(): void {
    FileUtil.checkDirWithoutDelete(this.intermediatesResDir!);
    FileUtil.checkDirWithoutDelete(this.generateSourceRDir!);
  }

  declareInputs(): Map<string, TaskInputValue> {
    const inputs = new Map<string, TaskInputValue>();
    if (this.isPreview) {
      inputs.set('PREVIEWER_REPLACE_PAGE', JSON.stringify(hvigorCore.getExtraConfig().get(InjectConst.PREVIEWER_REPLACE_PAGE)));
      inputs.set('PREVIEWER_REPLACE_SRCPATH', JSON.stringify(hvigorCore.getExtraConfig().get(InjectConst.PREVIEWER_REPLACE_SRC_PATH)));
    }
    inputs.set('TARGET_CONFIG', JSON.stringify(this.targetData.getTargetOpt()));
    return inputs;
  }

  declareExecutionTool(): string {
    return this.getResToolFile();
  }

  declareInputFiles(): FileSet {
    return this.restoolConfigBuilder.inputFiles;
  }

  declareOutputFiles(): FileSet {
    return this.restoolConfigBuilder.outputFiles.addEntries([this.intermediatesResDir!, this.generateSourceRDir!], {
      isDirectory: true,
    });
  }

  /**
   * get restool path
   *
   * @returns {string}
   */
  protected getResToolFile(): string {
    return this.sdkInfo.getRestool();
  }

  /**
   * 资源编译流程模板方法
   *
   * @param {string} commandType 命令类型（）
   * @param {ModuleTargetData} targetData
   * @protected
   */
  protected async compilationProcess(commandType: string, targetData: ModuleTargetData): Promise<void> {
    // 资源编译前置检查通过后才调用restool
    if (this.precheck(commandType)) {
      await this.invokeRestool(targetData);
    }
  }

  /**
   * 资源编译前置检查
   *
   * @param {string} commandType 构造资源编译命令的方式
   * @return {boolean} true 通过 false 不通过
   */
  precheck(commandType: string): boolean {
    // 资源命令中是否含有资源目录
    if (commandType === CompileResourceBuildCommandType.FILE) {
      return this.restoolConfigBuilder.existsResourceDir;
    } else {
      return !!this.restoolCommandBuilder?.existsResourceDir;
    }
  }

  /**
   * 调用restool执行资源编译，抽象方法
   *
   * @param {ModuleTargetData} targetData
   */
  async invokeRestool(targetData: ModuleTargetData): Promise<void> {
    // 此类不实现逻辑，由子类重新覆盖
  }

  /**
   * 同步执行命令
   *
   * @param {commands} commands 命令
   * @param {OhosLogger} log 日志
   * @param {Function} callback 编译资源完成后的回调函数
   * @protected
   */
  protected async executeCommand(commands: string[], log: OhosLogger, callback: Function = noop, spawnOptions?: SpawnSyncOptions): Promise<void> {
    let appBundleName;
    if (this.projectModel.isFaMode()) {
      appBundleName = this.moduleModel.getJsonObjByTargetName(this.targetData.getTargetName())?.app.bundleName;
    } else {
      appBundleName = this.projectModel.getDefaultBundleName();
    }
    log._printDebugCommand(this.getResToolFile(), replaceBundleName(commands, appBundleName));
    const subTaskName = 'execute compile resource command using restool';
    const subDurationEvent = this.durationEvent.createSubEvent(subTaskName, '');
    subDurationEvent.start();

    await new ProcessUtils(this.moduleName, this.name).execute(commands, spawnOptions, undefined, (result: SpawnSyncReturns<string>, ohosCharset: string) => {
      this.restoolReportHandler(result);
      return this.iconSizeWarningHandler(result, ohosCharset, log);
    });
    subDurationEvent.stop();
    subDurationEvent.setLog(subTaskName, MetricLogType.INFO);

    await callback();
  }

  /**
   * 资源编译特殊日志处理
   *
   * @param {execa.SpawnSyncReturns<string>} spawnReturnValue 执行命令返回结果
   * @param {string} ohosCharset 日志解析编码方式
   * @param {OhosLogger} log 日志对象
   * @return {boolean} true/false，处理成功返回无需后续通用日志打印
   * @private
   */
  private iconSizeWarningHandler(spawnReturnValue: SpawnSyncReturns<string>, ohosCharset: string, log: OhosLogger): boolean {
    if (!(spawnReturnValue.stderr?.length > 0)) {
      return false;
    }
    const stderrStr = spawnReturnValue.stderr;
    const regExpCase1 = /Warning: the png width and height not equal/;
    const regExpCase2 = /Warning: The width or height of the .+ file referenced by the icon exceeds the limit+./;
    if (!regExpCase1.test(stderrStr) && !regExpCase2.test(stderrStr)) {
      return false;
    }

    // iconCheck时，debug模式时才打印warning日志，其他场景不打印日志
    if (JSON.stringify(log.getLevel()) === JSON.stringify(levels.DEBUG)) {
      log.warn(stderrStr);
    }
    return true;
  }

  /**
   * restool 输出结果打点上报
   * @param spawnReturnValue
   * @private
   */
  private restoolReportHandler(spawnReturnValue: SpawnSyncReturns<string>) {
    const allRestoolOutput = (spawnReturnValue.stdout + spawnReturnValue.stderr).split('\n');
    const reportIndex = allRestoolOutput.findIndex((text) => text.startsWith('Processing report:'));
    if (reportIndex >= 0 && reportIndex + 3 < allRestoolOutput.length) {
      const numberRegex = /\d+\\.\d+|\d+/g;
      const compressed = allRestoolOutput[reportIndex + 2]?.match(numberRegex)?.map((str) => parseFloat(str));
      const success = allRestoolOutput[reportIndex + 3]?.match(numberRegex)?.map((str) => parseFloat(str));

      // 有至少一个文件参与转换才上报打点数据
      if (!Array.isArray(compressed) || !Array.isArray(success) || compressed[0] === 0 || success[2] === 0) {
        return;
      }
      const [transcodeFiles, transcodeTime] = compressed;
      const [successfulFiles, successTime, beforeBytes, afterBytes] = success;
      ohosTrace.traceRestoolCompression(this.moduleModel.getName(), {
        TIMESTAMP: Date.now(),
        TRANSCODE_FILES: transcodeFiles,
        TRANSCODE_SUCCESS_RATE: successfulFiles / transcodeFiles,
        TRANSCODE_TOTAL_TIME: transcodeTime,
        TRANSCODE_EXPANSION_RATE: afterBytes / beforeBytes,
      });
    }
  }

  /**
   * 以命令为参数执行编译、链接resTool命令
   *
   * @param {OhosLogger} log 日志对象
   * @param {Function} callback 编译资源完成后的回调函数
   * @protected
   */
  protected async compileLink(log: OhosLogger, callback: Function = noop): Promise<void> {
    // 编译
    for (const compile of this.compileCommands) {
      await this.executeCommand(compile, log);
    }

    // 链接
    await this.executeCommand(this.linkCommand!, log, callback);
  }

  private getInputData(commands: string[]) {
    return {
      moduleName: this.moduleName,
      taskName: this.name,
      commandLine: commands,
    };
  }

  /**
   * 执行preview资源编译、链接
   *
   * @param {OhosLogger} log 日志对象
   * @param {Function} callback 回调函数
   */
  async previewCompileLink(log: OhosLogger, callback: Function = noop): Promise<void> {
    const subTaskName = 'generate compilation link command';
    const subDurationEvent = this.durationEvent.createSubEvent(subTaskName, '');
    subDurationEvent.start();

    // 初始化参数
    this.restoolLinkParamObj = this.restoolCommandBuilder?.restoolLinkParamObj;

    // 构造编译、链接命令
    await this.buildIncrementalCompileLinkFullCommand();
    subDurationEvent.stop();
    subDurationEvent.setLog(subTaskName, MetricLogType.INFO);

    // 执行Restool 编译、链接
    await this.compileLink(log, callback);

    // Preview 构建参数持久化
    await this.previewBuildParamPersistence();
  }

  /**
   * Preview 构建参数持久化
   * （预览热加载时使用此参数）
   */
  async previewBuildParamPersistence() {
    const appResource = this.restoolLinkParamObj!.appScopeResources;
    const moduleResourceMap = this.restoolLinkParamObj!.moduleResourcesMap;
    const harResourceMap = this.restoolLinkParamObj!.harResourcesMap;

    const previewBuildParam: object = {
      APP_RESOURCES: appResource ? appResource : '',
      MODULE_RESOURCES: JSON.stringify(strMapToObj(moduleResourceMap as Map<string, string>)),
      HAR_RESOURCES: JSON.stringify(strMapToObj(harResourceMap as Map<string, string>)),
      PREVIEW_GENERATE_SOURCE_DIR: this.projectModel.isFaMode()
        ? this.getTaskTempDir(this.targetData, BuildDirConst.R, false)
        : this.pathInfo.getPreviewGenerateSourceR(),
      PREVIEW_COMPILE_MODULE_NAMES: CompileResourcesUtil.getRestoolModuleNames(this.service, this.targetName).join(','),
    };
    const jsonFilePath: string = path.resolve(this.pathInfo.getModulePreviewPath(), BuildArtifactConst.PREVIEW_BUILD_PARAM_JSON);
    await fse.outputJSON(jsonFilePath, previewBuildParam);
  }

  /**
   * 构建增量特性编译链接完整命令
   *
   * @protected
   */
  protected async buildIncrementalCompileLinkFullCommand() {
    this.linkCommand = this.restoolLinkParamObj!.linkCommand;
    this.linkOutputPath = this.restoolLinkParamObj?.outputPath;
    const appScopeResources = this.restoolLinkParamObj!.appScopeResources;
    const moduleResourcesMap = this.restoolLinkParamObj!.moduleResourcesMap;
    const harResourcesMap = this.restoolLinkParamObj!.harResourcesMap;
    const outputPath = this.restoolLinkParamObj!.outputPath;
    const idsMapPath = this.restoolLinkParamObj!.idsMapPath;

    // 清空输出目录，但保留restool资源增量编译固定id使用的ids_map目录
    await fse.ensureDir(outputPath!);
    const files: string[] = (await fse.readdir(outputPath!)).filter((filename) => filename !== BuildDirConst.IDS_MAP);
    for (const fileName of files) {
      await fse.remove(path.resolve(outputPath!, fileName));
    }

    if (appScopeResources && appScopeResources.length > 0) {
      const compiledOut: string = path.resolve(this.linkOutputPath as string, BuildDirConst.APP_COMPILED);

      const fullCommand = [this.getResToolFile(), '-x', appScopeResources, '-o', compiledOut];
      this.compileCommands.push(fullCommand);

      this.linkCommand!.push(...['-i', compiledOut]);

      FileUtil.checkDirWithoutDelete(compiledOut!);
    }

    // 该目录在API10才会生成
    if (idsMapPath) {
      FileUtil.checkDirWithoutDelete(idsMapPath!);
    }

    this.linkCommandPushResourcesMap(moduleResourcesMap!);
    this.linkCommandPushResourcesMap(harResourcesMap!);
    this.linkCommand!.push(...['-o', outputPath!]);

    // systemTheme下可配置hos/sdk下的资源，需要将hos/sdk的资源id传给restool
    if (!this.sdkInfo.isOhos) {
      this.linkCommand!.push(...['--defined-sysids', path.resolve(this.sdkInfo.getHosToolchainsDir(), 'id_defined.json')]);
    }

  }

  /**
   * 链接（link）命令添加ResourcesMap转换的命令
   *
   * @protected
   */
  protected linkCommandPushResourcesMap(resourcesMap: Map<string, string>) {
    if (resourcesMap.size > 0) {
      resourcesMap.forEach((output, resources) => {
        const compiledOut = path.resolve(this.linkOutputPath as string, output);

        const fullCommand = [this.getResToolFile(), '-x', resources, '-o', compiledOut];
        this.compileCommands.push(fullCommand);

        this.linkCommand!.push(...['-i', compiledOut]);

        if (!fse.existsSync(compiledOut)) {
          fse.mkdirsSync(compiledOut);
        }
      });
    }
  }

  /**
   * 根据entry的deviceType和当前feature的deviceType取交集后生成到config.json中
   * 若取交集后deviceType为空, 则报warning
   *
   * @param {ModuleTargetData} targetData
   * @param {string} entryName
   * @param {OhosLogger} _log
   * @protected
   */
  protected async retainDeviceType(targetData: ModuleTargetData, entryName: string, _log: OhosLogger): Promise<void> {
    // 获取entry的设备类型
    const entryTargetData = targetData.findRelatedTargetData(entryName);
    if (!checkHasTargetApplyProduct(this.targetData.getModuleModel(), this.targetData.getTargetName(), this.targetData.getProduct().name)) {
      return;
    }
    if (entryTargetData) {
      const entryDeviceTypes = entryTargetData.getTargetDeviceType();

      // 获取当前的设备类型
      const deviceTypes = targetData.getTargetDeviceType();

      // 取交集后重新生成config.json
      const retainedDeviceType = deviceTypes.filter((deviceType) => {
        return entryDeviceTypes?.indexOf(deviceType) > -1;
      });

      const configJson = path.resolve(targetData.getPathInfo().getIntermediatesRes(), entryName, CommonConst.CONFIG_JSON);
      const configJsonOpt: ConfigOptObj = getJson5Obj(configJson);
      configJsonOpt.module.deviceType = retainedDeviceType;

      if (this.isFaMode) {
        // FA模型feature的hap包中写入与之关联entry的distroFilter对象信息
        const projectModel = this.service.getProjectModel();
        const moduleModel = projectModel.getModuleModelByName(entryName);

        const configJson = path.resolve(
          moduleModel?.getProjectDir() ?? path.resolve(projectModel.getProjectDir(), entryName),
          `src/main/${CommonConst.CONFIG_JSON}`,
        );
        const configJsonObj: ConfigOptObj = resModelLoader.getConfigJson(configJson);
        const distroFilterObj = entryTargetData.getTargetOpt().config?.distroFilter ?? configJsonObj?.module?.distroFilter;
        if (distroFilterObj) {
          configJsonOpt.module.distroFilter = distroFilterObj;
        }
      }

      await fse.outputJson(configJson, configJsonOpt, { spaces: '\t' });

      // 若交集为空则warning
      if (retainedDeviceType.length === 0) {
        _log.warn(`The device types of ${this.moduleModel.getName()} and ${entryName} are different. ` + `Invalid HAP ${this.moduleModel.getName()}.`);
      }
    }
  }

  /**
   * 资源编译完成后处理target页面路由配置
   *
   * @param {ModuleTargetData} targetData
   * @param {OhosLogger} log
   * @return true 表示处理完成， false 未处理
   * @protected
   */
  protected replaceTargetPages(targetData: ModuleTargetData, log: OhosLogger) {
    // 处理target配置路由
    const pages = targetData.getTargetOpt().source?.pages;
    if (pages !== undefined) {
      this.replacePages(log, pages);
    }
  }

  /**
   * 处理shortcuts字段bundleName定制场景
   * @constructor
   * @protected
   */
  protected customizeShortcut() {
    const curProduct = this.targetData.getProduct();
    const nodeDir: string = this.moduleModel.getModule().getNodeDir();
    const moduleJsonPath = path.resolve(nodeDir, `src/main/${CommonConst.MODULE_JSON5}`);
    const moduleJsonOpt: ModuleOptObj = resModelLoader.getModuleJson(moduleJsonPath);
    const metadataObjList = moduleJsonOpt.module.metadata;
    const appBundleName = (this.projectModel as ProjectModelImpl).getAppRes().getAppResOpt().app.bundleName;
    if (!metadataObjList || metadataObjList.length === 0) {
      return;
    }
    metadataObjList.forEach((metadataObj) => {
      if (!metadataObj.resource) {
        return;
      }
      const profileName = parsingProfileName(metadataObj.resource);
      const profilePath = path.resolve(nodeDir, `src/main/${BuildDirConst.RESOURCES_BASE_PROFILE_DIR}/${profileName}.json`);
      const profileObj = getJson5Obj(profilePath);
      const shortcutsArr: ShortcutsObj[] = profileObj.shortcuts;
      if (!shortcutsArr || shortcutsArr.length === 0) {
        return;
      }
      const targetName = this.targetData.getTargetOpt().name;
      const intermediatesProfilePath = path.resolve(
        this.pathInfo.getModuleProductPath(),
        `intermediates/res/${targetName}/${BuildDirConst.RESOURCES_BASE_PROFILE_DIR}/${profileName}.json`,
      );

      shortcutsArr.forEach((shortcutsObj) => {
        const newShortCutObj: ShortcutsObj = shortcutsObj;
        const wantsArr: WantsObj[] = newShortCutObj.wants;
        if (!wantsArr || wantsArr.length === 0) {
          return;
        }
        wantsArr.forEach((wantsObj) => {
          if (appBundleName === wantsObj.bundleName) {
            const appOptFromConfig = cloneDeep(projectOhosConfigManager.getOverrides()?.appOpt);
            if (appOptFromConfig?.bundleName) {
              wantsObj.bundleName = appOptFromConfig.bundleName;
              return;
            }
            if (curProduct.bundleName) {
              // product中存在bundleName定制且shortcuts中want对象表示当前应用bundleName才进行替换
              wantsObj.bundleName = curProduct.bundleName;
            }
          }
        });
        if (!fse.existsSync(intermediatesProfilePath)) {
          fse.createFileSync(intermediatesProfilePath);
        }
        fse.writeJSONSync(intermediatesProfilePath, { newShortCutObj });
      });
    });
  }
}
